﻿using Gargoyle.Roton;
using Gargoyle.ZZT.File;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;

namespace Gargoyle.ZZT
{
    public partial class Game : Roton.Environment
    {
        internal enum RadiusType
        {
            Update,
            Explode,
            Cleanup
        }

        private delegate void RadiusFunction(Location center, Location location);

        private Video BoardVideo;
        private bool BusyInternal;
        private Display Display;
        private bool FadeIn;
        private bool ShownAbout = false;
        private bool UpdateScreenInternal;

        internal BoardCollection Boards;
        internal Board CurrentBoard;
        internal ElementCollection Elements;
        internal Info Info;
        internal Language Language;
        internal MessageConverter MessageConverter;
        internal Parameters Parameters;
        internal Size Size;
        internal Sounds Sounds;
        internal State State;
        internal ThingCollection Things;
        internal Tile[,] Tiles;
        internal Int16 Type;

        public Game(Video newVideo, Audio newAudio, Input newInput, Cache newCache, Int16 newType, bool useDisplay)
            : base(newVideo, newAudio, newInput, newCache)
        {
            Type = newType;
            switch (Type)
            {
                case -1:
                    base.Cache.Import(File.DataCache.Read(Properties.Resources.ZZTDAT)); 
                    Elements = new ElementCollection(ElementList.ZZT());
                    Parameters = ParameterList.ZZT();
                    Size = new Size(Parameters.BoardWidth + 2, Parameters.BoardHeight + 2);
                    BoardVideo = new Video(Size);
                    Display = new ZZTDisplay(BoardVideo, Video, this);
                    Tiles = new Tile[Size.Width, Size.Height];
                    MessageConverter = new MessageConverter(Parameters);
                    State = new State();
                    Language = LanguageList.ZZT();
                    Sounds = SoundList.ZZT();
                    InitAudio();
                    InitBorder();
                    break;
                case -2:
                    Elements = new ElementCollection(ElementList.SuperZZT());
                    Parameters = ParameterList.SuperZZT();
                    Size = new Size(Parameters.BoardWidth + 2, Parameters.BoardHeight + 2);
                    BoardVideo = new Video(Size);
                    Display = new SuperZZTDisplay(BoardVideo, Video, this);
                    Tiles = new Tile[Size.Width, Size.Height];
                    MessageConverter = new MessageConverter(Parameters);
                    State = new State();
                    Language = LanguageList.SuperZZT();
                    Sounds = SoundList.SuperZZT();
                    ShownAbout = true;
                    InitAudio();
                    InitBorder();
                    break;
                default:
                    throw new ArgumentException("Unrecognized engine ID.");
            }

            if (!useDisplay)
            {
                BoardVideo = Video;
                Display = null;
            }
        }

        public bool Busy
        {
            get
            {
                return BusyInternal;
            }
        }

        public void Cheat(ByteString cheatString)
        {
            byte modifier = 1;

            cheatString = UpperCase(cheatString);

            if (cheatString.Length > 0)
            {
                if (cheatString[0] == 0x2B) // +
                {
                    string flagString = cheatString;
                    cheatString = flagString.Substring(1);
                    SetFlag(cheatString);
                }
                else if (cheatString[0] == 0x2D) // -
                {
                    string flagString = cheatString;
                    cheatString = flagString.Substring(1);
                    ClearFlag(cheatString);
                    modifier = 0;
                }

                for (int i = 0; i < Language.CheatFunctions.Count; i++)
                {
                    if (Language.CheatNames[i] == cheatString)
                    {
                        Language.CheatFunctions[i](this, modifier);
                        break;
                    }
                }
            }

            PlaySound(10, Sounds.Cheat);
            UpdateStatus();
        }

        internal void ClearMessage()
        {

            int index = GetThingIndexAt(new Location(0, 0));
            if (index != -1)
            {
                if (Display != null)
                    Display.ClearMessage();

                if (Parameters.MessageOnBorder)
                {
                    byte top = 1;
                    byte left = 1;
                    byte right = (byte)(Size.Width - 2);
                    byte bottom = (byte)(Size.Height - 2);

                    for (byte x = left; x <= right; x++)
                    {
                        UpdateAt(new Location(x, top));
                        UpdateAt(new Location(x, bottom));
                    }
                    top++;
                    bottom--;
                    for (byte y = top; y <= bottom; y++)
                    {
                        UpdateAt(new Location(left, y));
                        UpdateAt(new Location(right, y));
                    }
                }
            }
        }

        internal int Difference(int i, int j)
        {
            return Math.Abs(i - j);
        }

        internal bool DisplayBusy
        {
            get
            {
                if (Display != null)
                {
                    return (Display.Busy);
                }
                return false;
            }
        }

        internal void DisplayChoice(SelectionParameter selectParm, int defaultVal)
        {
            Display.ShowChoice(selectParm, defaultVal);
        }

        internal bool DisplaySelecting
        {
            get
            {
                if (Display != null)
                {
                    return (Display.Selecting);
                }
                return false;
            }
        }

        internal AnsiChar Draw(Location location)
        {
            Tile tile = GetTileAt(location);
            Element element = Elements[tile.Element];

            // empty is always (0x20, 0x0F)
            if (element.Id == ElementID.Empty)
                return new AnsiChar(0x20, 0x0F);

            // use the element's specified draw code
            if (element.DrawCodeEnabled)
            {
                if (element.DrawCode != null)
                    return element.DrawCode(this, location);
                else
                    return new AnsiChar(0x4F, 0x40);
            }

            // text is handled specially
            switch (element.Id)
            {
                case ElementID.BlackText:
                    return new AnsiChar(tile.Color, 0x0F);
                case ElementID.BlueText:
                    return new AnsiChar(tile.Color, 0x1F);
                case ElementID.BrownText:
                    return new AnsiChar(tile.Color, 0x6F);
                case ElementID.CyanText:
                    return new AnsiChar(tile.Color, 0x3F);
                case ElementID.GreenText:
                    return new AnsiChar(tile.Color, 0x2F);
                case ElementID.PurpleText:
                    return new AnsiChar(tile.Color, 0x5F);
                case ElementID.RedText:
                    return new AnsiChar(tile.Color, 0x4F);
            }

            // default tile character
            return new AnsiChar(element.DefaultChar, tile.Color);
        }

        public override void Execute()
        {
			if (BusyInternal)
			{
				return;
			}
            BusyInternal = true;

            base.Execute();

            if (DisplayBusy)
            {
                if (DisplaySelecting)
                {
                    ReadInput();
                    UpdateDisplay();
                    if (!DisplaySelecting)
                    {
                        switch (Display.SelectParameter)
                        {
							case SelectionParameter.Cheat:
								Cheat(Display.SelectedString);
								break;
                            case SelectionParameter.GameSpeed:
                                State.Speed = Display.SelectedChoice;
                                break;
                            case SelectionParameter.LoadWorld:
                                if(Display.ScrollLine.Length > 0)
                                Load(Display.ScrollLine);
                                break;
							case SelectionParameter.QuitGame:
                                if(Display.SelectedChoice == 1)
                                {
                                    State.BreakGameLoop = true;
                                    State.Paused = false;
                                    StopSound();
                                }
                                else
                                    State.BreakGameLoop = false;
								RedrawStatus();
								break;
							case SelectionParameter.QuitZZT:
								if (Display.SelectedChoice == 1)
									State.QuitZZT = true;
								else
									State.QuitZZT = false;
								RedrawStatus();
								break;
						}
                        UpdateStatus();
                    }
                }
                else
                {
                    ReadInput();
                    UpdateDisplay();
                }
            }
            else if (FadeIn)
            {
                FadeIn = false;
                Display.ClearMessage();
                Display.FadeIn();
                UpdateDisplay();
                UpdateStatus();
            }
            else if (DisplaySelecting)
            {
                UpdateDisplay();
            }
            else
            {
				if (State.QuitZZT)
				{
					Quit();
				}
				else
				{
					ExecuteMain();
				}
            }

            ExecuteSound();
            BusyInternal = false;
        }

        internal void Fade()
        {
            RedrawBoard();
            if (Display != null)
            {
                FadeIn = true;
                Display.FadeOut();
            }
        }

        internal bool FindTile(Tile kind, ref Location location)
        {
            location.X++;
            while (location.Y < Size.Height)
            {
                while (location.X < Size.Width)
                {
                    if (TilesMatch(kind, GetTileAt(location)))
                        return true;
                    location.X++;
                }
                location.X = 0;
                location.Y++;
            }

            return false;
        }

        public int GameSpeed
        {
            get
            {
                return State.Speed;
            }
        }

        internal int GetAdjacents(Location location, byte element)
        {
            int result = 0;

            if ((location.Y <= 1) || GetLowerTile(location.Subtract(new Location(0, 1))).Element == element)
            {
                result += 1;
            }
            if ((location.Y >= Size.Height - 2) || GetLowerTile(location.Add(new Location(0, 1))).Element == element)
            {
                result += 2;
            }
            if ((location.X <= 1) || GetLowerTile(location.Subtract(new Location(1, 0))).Element == element)
            {
                result += 4;
            }
            if ((location.X >= Size.Width - 2) || GetLowerTile(location.Add(new Location(1, 0))).Element == element)
            {
                result += 8;
            }

            return result;
        }

        internal byte GetColorAt(Location location)
        {
            if (WithinBoard(location))
                return Tiles[location.X, location.Y].Color;
            return 0;
        }

        internal Element GetElementAt(Location location)
        {
            if (WithinBoard(location))
                return Elements[Tiles[location.X, location.Y].Element];
            return Elements[ElementID.BoardEdge];
        }

        private Tile GetLowerTile(Location location)
        {
            if (Parameters.AdjacentUnderThings)
            {
                int index = GetThingIndexAt(location);
                if (index >= 0)
                {
                    return Things[index].UnderTile;
                }
                else
                {
                    return GetTileAt(location);
                }
            }
            else
            {
                return GetTileAt(location);
            }
        }

        internal Thing GetThingAt(Location location)
        {
            int index = GetThingIndexAt(location);
            if (index >= 0)
                return Things[index];
            return new Thing();
        }

        internal int GetThingIndexAt(Location location)
        {
            for (int i = 0; i < Things.Count; i++)
            {
                if (Things[i].Location == location)
                    return i;
            }
            return -1;
        }

        internal Tile GetTileAt(Location location)
        {
            if (WithinBoard(location))
                return Tiles[location.X, location.Y];
            return new Tile(0, Elements.IndexOf(ElementID.BoardEdge));
        }

        private void InitAudio()
        {
            Audio.FreeSounds();
            Audio.LoadSound(Properties.Resources.drum0, true);
            Audio.LoadSound(Properties.Resources.drum1, true);
            Audio.LoadSound(Properties.Resources.drum2, true);
            Audio.LoadSound(Properties.Resources.player, false);
            Audio.LoadSound(Properties.Resources.drum4, true);
            Audio.LoadSound(Properties.Resources.drum5, true);
            Audio.LoadSound(Properties.Resources.drum6, true);
            Audio.LoadSound(Properties.Resources.drum7, true);
            Audio.LoadSound(Properties.Resources.drum8, true);
            Audio.LoadSound(Properties.Resources.drum9, true);
        }

        private void InitBorder()
        {
            Tile borderTile = new Tile(Elements.IndexOf(ElementID.BoardEdge), 0);
            for (byte x = 0; x < Size.Width; x++)
            {
                SetTileAt(new Location(x, 0), borderTile);
                SetTileAt(new Location(x, (byte)(Size.Height - 1)), borderTile);
            }
            for (byte y = 0; y < Size.Height; y++)
            {
                SetTileAt(new Location(0, y), borderTile);
                SetTileAt(new Location((byte)(Size.Width - 1), y), borderTile);
            }
        }

        public override void Load(string fileName)
        {
            if(fileName == "Cancel") return;
            byte[] gameData = Cache.LoadFile(fileName + "." + Parameters.WorldExtension);
            if (gameData != null)
            {
                MemoryStream stream = new MemoryStream(gameData);
                WorldConverter converter = new WorldConverter(stream, Parameters);
                World world = converter.Read(Type);
                if (world != null)
                {
                    Boards = world.Boards;
                    Elements = world.Elements;
                    Info = world.Info;
                    State = new State();
					State.InitGame = true;
					State.InitGameLoop = true;
                    State.StartBoard = Info.Board;
					State.EditorMode = false;
					SetInvisibleChar(false);
                    if (Info.SavedGame != 0)
                    {
						SetGameMode();
                    }
                    else
                    {
						SetTitleMode();
					}
                }
                else
                {
                    throw new InvalidOperationException("Unable to load the world file.");
                }
            }
            else
            {
                throw new FileNotFoundException("Unable to locate the world file.");
            }
        }

        internal void LoadBoard(int index)
        {
            if (index >= Boards.Count)
            {
                index = Info.Board;
            }
            Info.Board = (Int16)index;

            CurrentBoard = Boards[index];
            Things = CurrentBoard.Things;

            int boardTileIndex = 0;
            for (int y = 0; y < Parameters.BoardHeight; y++)
            {
                for (int x = 0; x < Parameters.BoardWidth; x++)
                {
                    Tiles[x + 1, y + 1] = CurrentBoard.Tiles[boardTileIndex++];
                }
            }
            SetTileAt(Things[0].Location, new Tile(Elements.IndexOf(State.PlayerElement), State.PlayerElement.DefaultColor));
        }

        public override string Name()
        {
            return (Info.Name + ": " + CurrentBoard.Name);
        }

        internal void PlotTile(Location location, Tile tile)
        {
            byte color;

            if (GetElementAt(location).Id != ElementID.Player)
            {
                Element element = Elements[tile.Element];
                color = tile.Color;
                if (element.DefaultColor >= 0xF0)
                {
                    if (color == 0)
                        color = GetColorAt(location);
                    if (color == 0)
                        color = 0x0F;
                    if (element.DefaultColor == 0xFE)
                        color = (byte)(((color - 8) << 4) + 0x0F);
                }
                else
                {
                    color = element.DefaultColor;
                }
                if (GetTileAt(location).Element == tile.Element)
                {
                    SetColorAt(location, color);
                }
                else
                {
                    DestroyAt(location);
                    if (element.Cycle < 0)
                    {
                        SetTileAt(location, new Tile(tile.Element, color));
                    }
                    else
                    {
                        SpawnThing(location, element, color, element.Cycle, null);
                    }
                }
                UpdateAt(location);
            }
        }

        internal int Polarity(int value)
        {
            if (value < 0)
            {
                return -1;
            }
            else if (value > 0)
            {
                return 1;
            }
            return 0;
        }

        internal int RandomNumber(int maxValue)
        {
            return Random.Next(maxValue);
        }

        internal void ReadInput()
        {
            Input.Poll();
            State.KeyPressed = Input.GetPressedKey();
            State.KeyShift = Input.Shift;
            State.KeyVector = ReadInputVector();
            if ((State.KeyVector.X != 0) || (State.KeyVector.Y != 0))
            {
                State.KeyLastVector = State.KeyVector;
                State.KeyArrow = true;
            }
            else
            {
                State.KeyArrow = false;
            }
        }

        private Vector ReadInputVector()
        {
            switch (State.KeyPressed)
            {
                case Keys.Down:
                    return Vector.South();
                case Keys.Left:
                    return Vector.West();
                case Keys.Right:
                    return Vector.East();
                case Keys.Up:
                    return Vector.North();
                default:
                    return Vector.Idle();
            }
        }

        internal void RedrawBoard()
        {
            for (byte y = 0; y < Size.Height; y++)
            {
                for (byte x = 0; x < Size.Width; x++)
                {
                    UpdateAt(new Location(x, y));
                }
            }
        }

        internal void RedrawStatus()
        {
            if (Display != null)
                Display.RedrawHud();
        }

        public override void Save(string fileName)
        {
            MemoryStream stream = new MemoryStream();
            WorldConverter converter = new WorldConverter(stream, Parameters);
            World world = new World(Type, Info, Boards, Elements, Parameters);
            converter.Write(world);
            if (stream.Length > 0)
            {
                Cache.SaveFile(fileName, stream.ToArray());
            }
        }

        internal void SaveBoard()
        {
            int boardTileIndex = 0;
            for (int y = 0; y < Parameters.BoardHeight; y++)
            {
                for (int x = 0; x < Parameters.BoardWidth; x++)
                {
                    CurrentBoard.Tiles[boardTileIndex++] = Tiles[x + 1, y + 1];
                }
            }
        }

        internal void SetBoard(int index)
        {
            if (CurrentBoard != null)
            {
                SetElementAt(Things[0].Location, Elements[ElementID.Player]);
                SetColorAt(Things[0].Location, Elements[ElementID.Player].DefaultColor);
                SaveBoard();
            }
            LoadBoard(index);
        }

        internal void SetColorAt(Location location, byte color)
        {
            Tiles[location.X, location.Y].Color = color;
        }

        internal void SetElementAt(Location location, Element element)
        {
            Tiles[location.X, location.Y].Element = Elements.IndexOf(element);
        }

		private void SetGameMode()
		{
			State.Paused = true;
			State.PlayerElement = Elements[ElementID.Player];
			State.TitleScreen = false;
            ClearSound();
			RedrawStatus();
			SetBoard(State.StartBoard);
			EnterBoard();
			Fade();
		}

        private void SetInvisibleChar(bool visible)
        {
            if (visible)
                Elements[ElementID.Invisible].DefaultChar = ElementPresets.Invisible().DefaultChar;
            else
                Elements[ElementID.Invisible].DefaultChar = 0x20;
        }

        internal void SetMessage(Int16 duration, ByteString message)
        {
            ClearMessage();

            if (message.Length > 0)
            {
                int index = GetThingIndexAt(new Location(0, 0));
                if (index >= 0)
                    RemoveThing(index);
                SpawnThing(new Location(0, 0), Elements[ElementID.Messenger], 0, 1, null);
                unchecked
                {
                    Things[Things.Count - 1].P2 = (byte)((duration / (State.WaitTime + 1)));
                }
                CurrentBoard.Message = message;
            }
        }

        internal void SetMessageInternal(Int16 duration, ByteString message, int value)
        {
            SetMessage(duration, MessageConverter.Convert(message, value));
        }

        internal void SetScroll(ByteString name, ByteString content)
        {
            if (Display != null)
                Display.ShowScroll(name, content);
        }

        internal void SetTileAt(Location location, Tile value)
        {
            Tiles[location.X, location.Y] = value;
        }

		private void SetTitleMode()
		{
			State.TitleScreen = true;
			State.PlayerElement = Elements[ElementID.Monitor];
			RedrawStatus();
			SetBoard(0);
			EnterBoard();
			Fade();
		}

        internal void ShowAbout()
        {
            ShownAbout = true;
            ShowHelp("ABOUT");
        }

        internal void ShowCheat()
        {
            DisplayChoice(SelectionParameter.Cheat, 0);
        }

		internal void ShowEndGame()
		{
			if (Info.Health > 0)
			{
				Display.PromptString = "End this game?";
				DisplayChoice(SelectionParameter.QuitGame, 0);
				State.KeyPressed = Keys.None;
			}
		}

		internal void ShowHelp(ByteString helpFileName)
        {
            byte[] helpData = Cache.LoadFile(helpFileName + "." + Parameters.HelpExtension);
            if (helpData != null && helpData.Length > 0)
            {
                SetScroll(helpFileName, helpData);
            }
        }

		internal void ShowQuitZZT()
		{
			Display.PromptString = "Quit ZZT?";
			DisplayChoice(SelectionParameter.QuitZZT, 0);
			State.KeyPressed = Keys.None;
		}

        public void SwitchBoard(int index)
        {
            if (index >= 0 && index < Boards.Count)
            {
                SetBoard(index);
                Fade();
            }
        }

        public int TicksPerCycle
        {
            get
            {
                return State.WaitTime;
            }
        }

        internal bool TilesMatch(Tile kind, Tile tile)
        {
            if (kind.Element == tile.Element)
            {
                if (kind.Color == 0)
                    return true;

                Element tileElement = Elements[tile.Element];
                if (tileElement.DefaultColor < 0xF0)
                    return (kind.Color == (tileElement.DefaultColor & 7));
                else if (tileElement.DefaultColor == 0xFE)
                    return (kind.Color == (((tile.Color >> 4) & 0x0F) + 8));
                else
                    return (kind.Color == (tile.Color & 0x0F));
            }

            return false;
        }

        internal void UpdateAt(Location location)
        {
            if ((location.X > 0) && (location.X < Size.Width - 1) && (location.Y > 0) && (location.Y < Size.Height - 1))
            {
                AnsiChar ac;
                Element element = GetElementAt(location);

                if ((CurrentBoard.Dark != 0) && (!State.EditorMode) && (!element.AlwaysShow) && (!WithinRadius(Things[0].Location, location) || Info.TorchCycles == 0))
                {
                    ac = new AnsiChar(0xB0, 0x07);
                }
                else
                {
                    ac = Draw(location);
                }
                BoardVideo.SetChar(location.ToPoint(), ac);
            }
        }

        internal void UpdateDisplay()
        {
            if (Display != null)
                Display.Update();
            if (Display.Animating())
                UpdateScreenInternal = true;
        }

        public bool UpdateScreen
        {
            get
            {
                bool result = UpdateScreenInternal;
                UpdateScreenInternal = false;
                return result;
            }
        }

        internal void UpdateStatus()
        {
            if (Display != null)
                Display.UpdateHud();
        }
    }
}
